#! /usr/local/bin/python
#-*- coding: utf-8 -*-

from __future__ import with_statement

"""
pyChatter. A multi-agent based instant messaging written in Python.
Copyright (C) 2010  Cedric Bonhomme

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

pyChatter  Copyright (C) 2010  Cedric Bonhomme
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
"""

__author__ = "Cedric Bonhomme"
__version__ = "$Revision: 0.1 $"
__date__ = "$Date: 2010/01/05 $"
__copyright__ = "Copyright (c) 2010 Cedric Bonhomme"
__license__ = "GPLv3"

import os
import time
import pickle
import hashlib
import threading
import tkMessageBox
import tkFileDialog

from Tkinter import *

import clientAgent

from security import rsa


fieldParams = {
    "height":10,
    "width":50,
    "font":("verdana",12),
    "highlightthickness":0,
    "borderwidth":2,
    "background":"white",
    "wrap":"word"
}
textParams = {
    "bg":"#F7E0D4",
    "fg":"#2321F1",
    "width":50,
    "height":2,
    "highlightthickness":0,
    "font":("verdana",12),
    "wrap":"word"
}
labelTextStyle = { \
    "fg" : "black", \
    "highlightthickness" :  0, \
    "font" : ("arial", 12) \
}

class SimpleGui(object):
    """Simple GUI for pyChatter.
    """
    def __init__(self, name, agent_host, agent_ontology, agent_pubkey):
        """
        Initializes GUI components.
        """
        self.user_name = name
        self.agent_name = name
        self.agent_host = agent_host
        self.ontology = agent_ontology
        self.pubkey = agent_pubkey
        self.rsa_nb_bits = 256
        self.my_rsa_keys = None
        self.client_agent = None

        self.master = Tk()
        self.master.protocol("WM_DELETE_WINDOW", self.onClose)
        if self.user_name:
            self.master.title("pyChatter - " + self.user_name)
        else:
            self.master.title("pyChatter")
        #self.master.minsize(width = 140, height = 40)
        self.master.resizable(height = False, width = False)

        # Create the menu
        self.__create_menu()

        # Text widget with a scrollBar for the conversation zone
        self.conversation_zone = Text(self.master, state = DISABLED, **fieldParams)
        self.scrollbar = Scrollbar(self.master, orient = VERTICAL)
        self.scrollbar.config(command = self.conversation_zone.yview)
        self.scrollbar.grid(column = 3, row = 1, sticky = S + N)
        self.conversation_zone.config(yscrollcommand = self.scrollbar)
        self.conversation_zone.grid(row = 1, columnspan = 3)

        # Text widget with scrollbar for the message to send
        self.message_to_send = Text(self.master, **textParams)
        self.barreM = Scrollbar(self.master, orient = VERTICAL)
        self.barreM.config(command = self.message_to_send.yview)
        self.barreM.grid(column = 3, row = 2, sticky = S + N)
        self.message_to_send.config(state = NORMAL, yscrollcommand = self.barreM)
        self.message_to_send.grid(row = 2, columnspan = 3, pady = 5)

        # Send button
        self.send = Button(self.master, text = 'Send', command = self.send_message)
        self.send.grid(row = 3, columnspan = 3, pady = 5)

        # Create the right panel
        self.__create_right_panel()

        # Status bar
        self.status = Label(self.master, \
                text = "You are not connected to the SPADE platform. " + \
                "Connect to the platform via the 'File' menu.", \
                bd = 1, relief = SUNKEN, anchor = W)
        self.status.grid(row = 4, columnspan = 5, sticky = W+E+N+S, pady = 5)

        # Main loop
        self.master.mainloop()


    def __create_menu(self):
        """Main menu
        """
        self.menu = Menu(self.master)
        self.master.config(menu=self.menu)

        # File menu
        filemenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "File", menu = filemenu)
        filemenu.add_command(label = "Connect to the SPADE platform", \
                                command = self.connect_to_platform)
        filemenu.add_command(label = "Disconnect from the SPADE platform", \
                                command = self.disconnect_from_platform)
        filemenu.add_separator()
        filemenu.add_command(label = "Signin", command = self.signin)
        filemenu.add_command(label = "Signout")
        filemenu.add_separator()
        filemenu.add_command(label = "Login", command = self.login)
        filemenu.add_separator()
        niveau = Menu(self.menu, tearoff = 0)
        filemenu.add_cascade(label = "State", menu = niveau)
        niveau.add_command(label = "Free to chat")
        niveau.add_command(label = "Not here")
        filemenu.add_separator()
        filemenu.add_command(label = "Quit", command = self.onClose)

        # Edit menu
        editmenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "Edit", menu = editmenu)
        editmenu.add_command(label = "Save conversation", command = self.save_conversation)
        editmenu.add_command(label = "Clear conversation zone", command = self.clear_window)

        # Management of interlocutors menu
        managementmenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "Management of interlocutors", \
                                                        menu = managementmenu)
        managementmenu.add_command(label = "Available interlocutors", \
                                                        command = self.display_client_list)
        managementmenu.add_command(label = "Blocked interlocutors", \
                                                        command = self.display_client_list)

        # Security menu
        securitymenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "Security", menu = securitymenu)
        securitymenu.add_command(label = "Generate RSA keys", command = self.generate_rsa_keys)

        # Help menu
        helpmenu = Menu(self.menu, tearoff = 0)
        self.menu.add_cascade(label = "Help", menu = helpmenu)
        helpmenu.add_command(label = "About", command = self.about)

    def __create_right_panel(self):
        """
        """
        self.connection_info = Label(self.master, text = "No interlocutor", \
                padx = 10, pady = 10, justify = LEFT, **labelTextStyle)
        self.connection_info.grid(row = 1, column = 4, sticky = W)

    def connect_to_platform(self):
        """
        """
        def execute():
            try:
                username = str(username_entry.get())
                address = str(spade_address_entry.get())
                password = str(spade_password_entry.get())
            except ValueError:
                tkMessageBox.showerror('Error', 'Bad entry')
                return

            self.user_name = username
            self.agent_name = username.lower().replace(' ', '_')

            self.client_agent = clientAgent.ClientAgent( \
                        self.agent_name + "@" + address, password)
            self.client_agent.start()
            self.client_agent.gui = self

            self.client_agent.launch_behaviour_authenticator()

            msg.destroy()

            self.master.title("pyChatter - " + self.user_name)

            self.status.config(text = "Connected to the SPADE platform. " + \
                                "Sign-in or just log yourself if you " + \
                                "are already registered.")

        if self.client_agent != None:
            self.show_info('Already connected to the SPADE platform.')
            return

        msg = Toplevel(self.master)
        msg.title('Connection to the SPADE platform')
        msg.resizable(height = False, width = False)

        Label(msg, text='Your username : ').grid(row = 0, column = 0, sticky = W)
        username_entry = Entry(msg)
        username_entry.grid(row = 0, column = 1, pady = 2, sticky = W)
        username_entry.insert(0, self.agent_name.lower())
        username_entry.focus_set()

        Label(msg, text='SPADE platform address : ').grid(row = 1, column = 0, sticky = W)
        spade_address_entry = Entry(msg)
        spade_address_entry.grid(row = 1, column = 1, pady = 2, sticky = W)
        spade_address_entry.insert(0, self.agent_host)

        Label(msg, text='SPADE password : ').grid(row = 2, column = 0, sticky = W)
        spade_password_entry = Entry(msg)
        spade_password_entry.grid(row = 2, column = 1, pady = 2, sticky = W)
        spade_password_entry.insert(0, "secret")

        Button(msg, text="OK", \
            command = execute).grid(row = 3, columnspan = 2, pady = 2)

        self.set_top_level_window(msg)


    def disconnect_from_platform(self):
        """
        """
        try:
            self.client_agent.tearDown()
            self.client_agent = None
            self.user_name = ''
            self.agent_name = ''
            self.master.title('pyChatter')
        except AttributeError:
            # agent not started
            self.show_info('Not connected to the SPADE platform.')

    def signin(self):
        """Signin Window
        Allows the client to register itself at the main server.
        """
        def execute():
            try:
                username = username_entry.get()
                password1 = password1_entry.get()
                password2 = password2_entry.get()
                self.ontology = ontology_entry.get()
                self.load_rsa_keys()
            except ValueError:
                tkMessageBox.showerror('Error', 'Bad entry')
                return

            if password1 != password2:
                self.show_info('The passwords should be the same.')
                return

            if len(password1) < 6:
                self.show_info('The password must be at least 6 caracters.')
                return

            if self.my_rsa_keys == None:
                self.show_info('You must generate your RSA keys.')
                return

            # set the outgoing message
            pubkey = str(self.my_rsa_keys.b) + "-" + str(self.my_rsa_keys.n)

            sha256_hash = hashlib.sha256()
            sha256_hash.update(password1)
            hashed_passwd = sha256_hash.hexdigest()

            self.client_agent.outgoing_message = "signin:" + \
                                        self.agent_name + \
                                        ":" + hashed_passwd + \
                                        ":" + self.ontology + \
                                        ":" + pubkey

            # send the message with a OneShotBehaviour
            self.client_agent.addBehaviour(self.client_agent.SendMessageToAuthenticator(), None)

            msg.destroy()


        if self.client_agent == None:
            self.show_info('You are not connected to the SPADE platform.')
            return

        msg = Toplevel(self.master)
        msg.title('Signin')
        msg.resizable(height = False, width = False)

        Label(msg, text='Username : ', state = DISABLED).grid(row = 0, column = 0, sticky = W)
        username_entry = Entry(msg)
        username_entry.grid(row = 0, column = 1, pady = 2, sticky = W)
        username_entry.insert(0, self.agent_name)
        username_entry.configure(state = DISABLED)
        username_entry.focus_set()

        Label(msg, text='Password : ').grid(row = 1, column = 0, sticky = W)
        password1_entry = Entry(msg)
        password1_entry.configure(show='*')
        password1_entry.grid(row = 1, column = 1, pady = 2, sticky = W)

        Label(msg, text='Password : ').grid(row = 2, column = 0, sticky = W)
        password2_entry = Entry(msg)
        password2_entry.configure(show='*')
        password2_entry.grid(row = 2, column = 1, pady = 2, sticky = W)

        Label(msg, text='Ontology : ').grid(row = 3, column = 0, sticky = W)
        ontology_entry = Entry(msg)
        ontology_entry.grid(row = 3, column = 1, pady = 2, sticky = W)
        ontology_entry.insert(0, self.ontology)

        Scale(msg, length=450, orient = HORIZONTAL, \
            sliderlength = 25, label = 'RSA key length :', \
            from_=128, to=3072, tickinterval = 256, \
            resolution = 128, showvalue = self.rsa_nb_bits, \
            command = self.rsa_key_length).grid(row = 4, columnspan = 2, \
                                                pady = 2, sticky = W)

        Button(msg, text="Generate RSA key", \
            command = self.generate_rsa_keys).grid(row = 5, columnspan = 2, pady = 2)

        Button(msg, text="OK", \
            command = execute).grid(row = 6, columnspan = 2, pady = 2)

        self.set_top_level_window(msg)

    def signout(self):
        """
        """
        pass

    def login(self):
        def execute():
            try:
                password = password_entry.get()
            except ValueError:
                tkMessageBox.showerror('Error', 'Bad entry')
                return

            sha256_hash = hashlib.sha256()
            sha256_hash.update(password)
            hashed_passwd = sha256_hash.hexdigest()

            self.client_agent.outgoing_message = "login:" + \
                self.agent_name + ":" + hashed_passwd

            # send the answer to the authentication agent
            self.client_agent.addBehaviour(self.client_agent.SendMessageToAuthenticator(), None)

            msg.destroy()

            if self.my_rsa_keys == None:
                self.load_rsa_keys()

        if self.client_agent == None:
            self.show_info('You are not connected to the SPADE platform.')
            return

        msg = Toplevel(self.master)
        msg.title('Login - ' + self.user_name)
        msg.resizable(height = False, width = False)

        Label(msg, text='Enter your password : ').grid(row = 0, column = 0)
        password_entry = Entry(msg)
        password_entry.configure(show='*')
        password_entry.grid(row = 1, column = 0, pady = 2)
        password_entry.focus_set()

        Button(msg, text="OK", \
            command = execute).grid(row = 5, columnspan = 2, pady = 2)

        self.set_top_level_window(msg)

    def authentication_success(self):
        """
        """
        self.status.config(text = "You are now ready to chat. " + \
                            "Select interlocutors via the" + \
                            " 'Management of interlocutors' menu.")
        self.show_info("Authentication success")

    def display_client_list(self):
        """Display the list of client in a new window.
        """
        try:
            list_of_clients = self.client_agent.search_agent_into_df( \
                                                    service_type = "client_agent")
        except AttributeError:
            self.show_info('You are not connected to the SPADE platform.')
            return

        msg = Toplevel(self.master)
        msg.title('Available clients')
        msg.resizable(height = False, width = False)

        Label(msg, text='Select a client in the list below.')

        # listbox for the list of registerd clients (to the DF)
        listboxScroll = Scrollbar(msg, orient = VERTICAL)
        listboxScroll.grid(row = 2, column = 2, sticky = W)
        self.listbox = Listbox(msg, yscrollcommand = listboxScroll.set, \
                                                    height = 1, width = 60)
        self.listbox.grid(row = 2, column = 1, sticky = W + E)
        listboxScroll['command'] = self.listbox.yview


        for client in list_of_clients:
            if self.client_agent.getAID().getName() != client.getAID().getName():
                name = client.getAID().getName().split('@')[0]
                if name in self.client_agent.blocked_sender:
                    self.listbox.insert(END, name + " - Blocked")
                else:
                    self.listbox.insert(END, name)


        self.listbox.bind("<Double-Button-1>", self.connect_to_client)
        self.listbox.bind("<Double-Button-3>", self.block_sender)

        Button(msg, text = 'Add/Remove', command = self.connect_to_client). \
                    grid(row = 3, column = 1, sticky = W+E+N+S, pady=5, padx=5)
        Button(msg, text = 'Block/Unblock', command = self.block_sender). \
                    grid(row = 4, column = 1, sticky = W+E+N+S, pady=5, padx=5)

        Button(msg, text = 'OK', command = msg.destroy). \
                    grid(row = 5, columnspan = 2, pady=5, padx=5)

        self.set_top_level_window(msg)

    def block_sender(self, event = None):
        """
        Block the selected client.
        Messages from this client will be ignored.
        """
        client = None
        try:
            client = self.listbox.get(int(self.listbox.curselection()[0]))
        except IndexError:
            pass
        if client is not None:
            if client not in self.client_agent.blocked_sender:
                self.client_agent.blocked_sender.append(client)
            else:
                self.client_agent.blocked_sender.remove(client)

    def connect_to_client(self, event = None, client = None):
        """
        Get the selected client (interlocutor) in the listbox.
        And ask to the authentication agent for informations avout
        the selected client.
        """
        if client is None:
            try:
                client = self.listbox.get(int(self.listbox.curselection()[0]))
            except IndexError:
                pass
        if client not in self.client_agent.receiver.keys():
            # If client is not already an interlocutor
            # ask for information to the authenticationAgent
            # set the outgoing message
            self.client_agent.outgoing_message = "addinterlocutor:" + client

            # send the answer to the authentication agent
            self.client_agent.addBehaviour(self.client_agent.SendMessageToAuthenticator(), None)
            # we are asking for the ontology and pubkey of the
            # selected client. The answer is managed in the
            # method "add_client"

            #tkMessageBox.showinfo('Information', 'Connected with ' + client)
        else:
            # If the selected client is already an interlocutor
            # remove it from the dic of interlocutors
            #tkMessageBox.showinfo('Information', 'Already connected with ' + client)
            del self.client_agent.receiver[client]
            self.update_connection_info_label()

    def add_client(self, client, ontology, pubkey):
        """
        The method is called by the client agent when the
        authentication agent has given the requested informations
        about the new interlocutor.
        """
        if ontology == 'None' and pubkey == 'None':
            self.show_info(client + ' is not registered.\n' + \
                'You would not be able to send message to ' + client + '.')
            return

        # Construct the RSA key of the new interlocutor
        rsa_interlocutor = rsa.RSA()
        rsa_interlocutor.b = int(pubkey.split('-')[0])
        rsa_interlocutor.n = int(pubkey.split('-')[1])

        # Add the interlocutor with its ontology and RSA public KEY
        self.client_agent.receiver[client] = (ontology, \
                                                    rsa_interlocutor)
        # Update the connection info label
        self.update_connection_info_label()

    def update_connection_info_label(self):
        """
        Update the label wich contains the
        list of current interlocutors.
        """
        if self.client_agent.receiver:
            if len(self.client_agent.receiver) == 1:
                connection_info = "Interlocutor:\n"
            else:
                connection_info = "Interlocutors:\n"
            for a_client in self.client_agent.receiver.keys()[:-1]:
                connection_info += " - " + a_client + ";\n"
            connection_info += " - " + self.client_agent.receiver.keys()[-1] + "."
            self.connection_info.configure(text=connection_info)
        else:
            self.connection_info.configure(text="No interlocutor")

    def send_message(self):
        """
        Send the message to the list of receivers (interlocutors).
        """
        message = str(self.message_to_send.get(0.0, END).encode('utf-8'))
        self.message_to_send.delete(1.0, END)

        if self.client_agent is None:
            self.show_info('You are not connected to the SPADE platform.')
            return

        if len(message) <= 1:
            self.show_info('Enter a message.')
            return

        if len(self.client_agent.receiver) == 0:
            self.show_info('You must have at least one interlocutor.')
            return

        if self.my_rsa_keys is None:
            self.show_info('Your RSA key can not be loaded.\n' + \
                            'Generate one or register yourself ' + \
                            'via the file menu')
            return


        # Now, the message can be sent
        local_time = time.strftime(" [%H:%M:%S]: ", time.localtime())

        # set the outgoing message (plain text)
        self.client_agent.outgoing_message = self.user_name + local_time + message

        # update the conversation zone
        self.update_conversation_zone("Me " + local_time + message)

        # send the message with a OneShotBehaviour
        self.client_agent.addBehaviour(self.client_agent.SendMessage(), None)

    def receive_message(self, acl_msg):
        """
        Called by clientAgent when a new message
        from another clent arrives. Test if the client
        is already an interlocutor. If not, propose to add
        this client in the dic of interlocutors.
        """
        name = acl_msg.getSender().getName().split('@')[0]
        dec_message = self.my_rsa_keys.decrypt_text(acl_msg.getContent())
        if name in self.client_agent.receiver.keys():
            self.update_conversation_zone(dec_message)
        else:
            if tkMessageBox.askyesno('Information', name + \
                                ' would like to chat with you.\n' + \
                                'Add this person to the conversation ?') :

                # Try to connect to the new client. This step will
                # success if the client is already registered.
                self.connect_to_client(client = name)

                # Display the message even if the connection
                # failed. Answer will not be possible in that
                # case.
                self.update_conversation_zone(dec_message)

    def update_conversation_zone(self, msg):
        """
        Update the conversation zone with the message 'msg'.
        """
        self.conversation_zone.config(state=NORMAL)
        self.conversation_zone.insert(END, msg)
        self.conversation_zone.config(state=DISABLED)

    def rsa_key_length(self, nb):
        """
        Change the length of the RSA key.
        """
        self.rsa_nb_bits = int(nb)

    def generate_rsa_keys(self):
        """
        Generate new pairs of RSA keys.
        """
        rsa_key = rsa.RSA(nb_bits = self.rsa_nb_bits)

        with open("./security/keys" + self.user_name, "w") as pub_key:
            pickle.dump(rsa_key, pub_key)

    def load_rsa_keys(self):
        """
        Load the RSA keys of the local client.
        """
        with open("./security/keys" + self.user_name, "r") as keys:
            self.my_rsa_keys = pickle.load(keys)
            print "RSA keys loaded."

    def clear_window(self):
        """
        Clear the conversation zone.
        """
        self.conversation_zone.delete(1.0, END)

    def save_conversation(self):
        """
        Save the conversation in a file.
        """
        if len(self.conversation_zone.get(1.0, END)) <= 1:
            self.show_info("There is no conversation.")
            return

        formats = [ \
        ('Format', '*.txt'), \
        ('All', '*.*') \
        ]

        local_time = time.strftime("%y-%m-%d_%H:%M:%S", time.localtime())

        text_file = tkFileDialog.asksaveasfile(parent = self.master, \
            filetypes = formats, title = "Save the conversation", mode='w', initialfile=local_time)
        if text_file != None:
            text = str(self.conversation_zone.get(1.0, END).encode('utf-8'))
            try:
                text_file.write('%s' % text)
                text_file.close()
            except :
                print "Writing error"
            (filepath, filename) = os.path.split(text_file.name)
            print text_file.name, "saved."

    def show_info(self, message):
        """
        Display an information message.
        """
        tkMessageBox.showinfo('Information', message)

    def onClose(self):
        """Close the client.
        """
        try:
            self.client_agent.tearDown()
        except AttributeError:
            # agent not started
            pass
        finally:
            self.master.destroy()

    def set_top_level_window(self, window):
        """
        """
        ## Sets the focus to the window (required for Windows)
        window.focus_set()
        ## Force events going on this window
        window.grab_set()
        ## For the window remains above its parent window
        window.transient(self.master)
        ## Displays the window and waits for closure
        window.wait_window(window)

    def about(self):
        """
        About message.
        """
        message = u"pyChatter"+u"\n\nCopyright (C) 2010\n\nCédric Bonhomme\n" \
        "\nhttp://pychatter.wikidot.com/\n" \
        "\nLicense : GPL Version 3, 29 June 2007"

        msg = Toplevel(self.master)
        msg.title('About')
        msg.resizable(height = False, width = False)
        Message(msg, width = 300, aspect = 100, justify = CENTER, \
        text = message).pack(padx = 10, pady = 10)

        self.set_top_level_window(msg)




if __name__ == "__main__":
    # Point of entry in execution mode
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option("-n", "--name", dest="name",
                    help="Name of the client agent")
    parser.add_option("-o", "--ontology", dest="ontology",
                    help="Ontology of the client agent")
    parser.add_option("-k", "--pub-Key", dest="pub_key",
                    help="Public key of the client agent")
    parser.add_option("-s", "--agent-host", dest="agent_host",
                    help="Address of the SPADE platform")


    parser.set_defaults(name = '', \
                        ontology = 'Chat', \
                        pub_key = '42', \
                        agent_host = '127.0.0.1')

    (options, args) = parser.parse_args()


    SimpleGui(options.name, options.agent_host, \
                options.ontology, options.pub_key)